﻿/* Copyright (C) 2007-2010 STS Soft

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 of the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
using STSdb.General.Collections;
using STSdb.General.Reflection;
using STSdb.Persist;

namespace STSdb.Data
{
    [Serializable]
    public class DefaultBinaryPersist<TData> : ISerializable, IBinaryPersist<IIndexer<TData>>
    {
        //C# 4 , IColumnDefinition<object>
        private Action<BinaryWriter, IIndexer<TData>, IColumnDefinition>[] stores;
        private Action<BinaryReader, IIndexer<TData>, IColumnDefinition>[] loads;
        private FactoryReflector<TData> factory;
        private IIndexerPersist<TData> persist;

        public IColumnDefinition[] Definitions { get; private set; }

        public DefaultBinaryPersist(Type persistType)
        {
            if (persistType == null)
                throw new ArgumentNullException("persistType");

            Definitions = new IColumnDefinition[0];

            Define(persistType);
        }

        //TODO: in C# 4 replace the parameters
        public DefaultBinaryPersist(params IColumnDefinition[] definitions)
        {
            if (definitions == null)
                throw new ArgumentNullException("definitions");

            Definitions = definitions;

            if (Definitions.Length > 0)
                Define(null); //user defined Column Definitions
            else
            {
                Type type = typeof(TData);
                Type persistType = PersistMap.GetDefaultPersist(type);
                if (persistType == null)
                {
                    if (type.IsValueType)
                        throw new NotSupportedException(String.Format("Type {0} contains non-value fields OR contains fields with [StructLayout(LayoutKind.Auto)] attribute. If you store structs make sure, that all struct fields are value-types AND StructLayout attribute is not set to LayoutKind.Auto.", type));

                    Definitions = BuildDefinitions(type); //typeof(TData) not recognized, try to build Definitions
                }

                Define(persistType);
            }
        }

        #region ISerializable Members

        protected DefaultBinaryPersist(SerializationInfo info, StreamingContext context)
        {
            if (!info.GetBoolean("is_persist"))
            {
                Definitions = new IColumnDefinition[info.GetInt32("column_definitions_count")];
                for (int i = 0; i < Definitions.Length; i++)
                    Definitions[i] = (IColumnDefinition)info.GetValue("column_definitions_" + i, typeof(IColumnDefinition));
                Define(null);
            }
            else
            {
                Definitions = new IColumnDefinition[0];

                string assemblyQualifiedName = info.GetString("persist");
                Define(Type.GetType(assemblyQualifiedName, true));
            }
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (persist == null)
            {
                info.AddValue("is_persist", false);
                info.AddValue("column_definitions_count", Definitions.Length);
                for (int i = 0; i < Definitions.Length; i++)
                    info.AddValue("column_definitions_" + i, Definitions[i]);
            }
            else
            {
                info.AddValue("is_persist", true);
                info.AddValue("persist", persist.GetType().AssemblyQualifiedName);
            }
        }

        #endregion

        private IColumnDefinition[] BuildDefinitions(Type dataType)
        {
            List<KeyValuePair<Type, string>> properties = PersistMap.GetNestedProperties(dataType);

            List<IColumnDefinition> definitions = new List<IColumnDefinition>(properties.Count);
            foreach (var kv in properties)
            {
                Type propertyType = kv.Key;
                string propertyName = kv.Value;
                
                Type persistType = PersistMap.GetDefaultPersist(propertyType);
                if (persistType == null) //unsupported property type, skip
                    continue;

                Type definitionType = typeof(DefaultColumnDefinition<,>).MakeGenericType(dataType, propertyType);
                IColumnDefinition instance = (IColumnDefinition)Activator.CreateInstance(definitionType, propertyName, persistType);

                definitions.Add(instance);
            }

            return definitions.ToArray();
        }

        private void Define(Type persistType)
        {
            if (persistType != null)
                persist = (IIndexerPersist<TData>)Activator.CreateInstance(persistType, false);
            else
            {
                factory = new FactoryReflector<TData>();

                stores = new Action<BinaryWriter, IIndexer<TData>, IColumnDefinition>[Definitions.Length];
                for (int i = 0; i < Definitions.Length; i++)
                {
                    Type columnType = Definitions[i].GetType().GetGenericArguments()[1];
                    persistType = Definitions[i].PersistType;
                    if (persistType == null)
                        persistType = PersistMap.GetDefaultPersist(columnType);

                    var writerParam = Expression.Parameter(typeof(BinaryWriter), "writer");
                    var dataParam = Expression.Parameter(typeof(IIndexer<TData>), "data");
                    var definitionParam = Expression.Parameter(typeof(IColumnDefinition), "definition");
                    Type columnDefinitionType = typeof(IColumnDefinition<,>).MakeGenericType(typeof(TData), columnType);
                    var definitionInstance = Expression.Convert(definitionParam, columnDefinitionType);

                    var persistInstance = Expression.New(persistType);
                    ConstructorInfo ci = (typeof(ColumnIndexer<,>).MakeGenericType(typeof(TData), columnType)).GetConstructor(new Type[] { typeof(IIndexer<TData>), columnDefinitionType });
                    var columnIndexer = Expression.New(ci, dataParam, definitionInstance);

                    MethodInfo methodInfo = persistType.GetMethod("Store");
                    var persistInstanceStoreCall = Expression.Call(persistInstance, methodInfo, writerParam, columnIndexer);
                    var lambdaStore = Expression.Lambda<Action<BinaryWriter, IIndexer<TData>, IColumnDefinition>>(persistInstanceStoreCall, writerParam, dataParam, definitionParam);
                    stores[i] = lambdaStore.Compile();
                }

                loads = new Action<BinaryReader, IIndexer<TData>, IColumnDefinition>[Definitions.Length];
                for (int i = 0; i < Definitions.Length; i++)
                {
                    Type columnType = Definitions[i].GetType().GetGenericArguments()[1];
                    persistType = Definitions[i].PersistType;
                    if (persistType == null)
                        persistType = PersistMap.GetDefaultPersist(columnType);

                    var readerParam = Expression.Parameter(typeof(BinaryReader), "reader");
                    var dataParam = Expression.Parameter(typeof(IIndexer<TData>), "data");
                    var definitionParam = Expression.Parameter(typeof(IColumnDefinition), "definition");
                    Type columnDefinitionType = typeof(IColumnDefinition<,>).MakeGenericType(typeof(TData), columnType);
                    var definitionInstance = Expression.Convert(definitionParam, columnDefinitionType);

                    var persistInstance = Expression.New(persistType);
                    ConstructorInfo ci = (typeof(ColumnIndexer<,>).MakeGenericType(typeof(TData), columnType)).GetConstructor(new Type[] { typeof(IIndexer<TData>), columnDefinitionType });
                    var columnIndexer = Expression.New(ci, dataParam, definitionInstance);

                    MethodInfo methodInfo = persistType.GetMethod("Load");
                    var persistInstanceLoadCall = Expression.Call(persistInstance, methodInfo, readerParam, columnIndexer);
                    var lambdaStore = Expression.Lambda<Action<BinaryReader, IIndexer<TData>, IColumnDefinition>>(persistInstanceLoadCall, readerParam, dataParam, definitionParam);
                    loads[i] = lambdaStore.Compile();
                }
            }
        }

        #region IBinaryPersist<IIndexer<TData>> Members

        public void Store(BinaryWriter writer, IIndexer<TData> data)
        {
            if (Definitions.Length == 0)
                persist.Store(writer, data);
            else
            {
                for (int i = 0; i < stores.Length; i++)
                    stores[i](writer, data, Definitions[i]);
            }
        }

        public void Load(BinaryReader reader, ref IIndexer<TData> data)
        {
            if (Definitions.Length == 0)
                persist.Load(reader, data);
            else
            {
                for (int i = 0; i < data.Count; i++)
                    data[i] = factory.CreateInstance();

                for (int i = 0; i < stores.Length; i++)
                    loads[i](reader, data, Definitions[i]);
            }
        }

        #endregion
    }

    class ColumnIndexer<TData, TField> : IIndexer<TField>
    {
        private IIndexer<TData> data;
        private IColumnDefinition<TData, TField> definition;

        public ColumnIndexer(IIndexer<TData> data, IColumnDefinition<TData, TField> definition)
        {
            this.data = data;
            this.definition = definition;
        }

        #region IIndexer<TField> Members

        public TField this[int index]
        {
            get { return definition.GetFieldValue(data[index]); }
            set { definition.SetFieldValue(data[index], value); }
        }

        public int Count
        {
            get { return data.Count; }
        }

        #endregion
    }
}
